<?php

namespace app\middleware;

use think\Request;
use think\Response;
use think\facade\Cache;
use Closure;

class RateLimit
{
    /**
     * 处理请求
     */
    public function handle(Request $request, Closure $next)
    {
        $key = $this->getRateLimitKey($request);
        $limit = $this->getRateLimit($request);
        $window = $this->getRateLimitWindow($request);

        if (!$this->checkRateLimit($key, $limit, $window)) {
            return $this->rateLimitExceeded();
        }

        return $next($request);
    }

    /**
     * 获取限流key
     */
    private function getRateLimitKey(Request $request)
    {
        $ip = $request->ip();
        $userId = $request->userId ?? 'guest';
        $path = $request->pathinfo();

        // 根据不同的接口使用不同的key策略
        if ($this->isAuthEndpoint($path)) {
            // 认证接口使用IP限制
            return "rate_limit:auth:{$ip}";
        } elseif ($request->userId) {
            // 已登录用户使用用户ID限制
            return "rate_limit:user:{$userId}";
        } else {
            // 未登录用户使用IP限制
            return "rate_limit:ip:{$ip}";
        }
    }

    /**
     * 获取限流次数
     */
    private function getRateLimit(Request $request)
    {
        $path = $request->pathinfo();

        // 不同接口不同限制
        if ($this->isAuthEndpoint($path)) {
            // 认证接口：每分钟10次
            return 10;
        } elseif ($this->isDataEndpoint($path)) {
            // 数据接口：每分钟100次
            return 100;
        } else {
            // 其他接口：每分钟60次
            return 60;
        }
    }

    /**
     * 获取限流时间窗口（秒）
     */
    private function getRateLimitWindow(Request $request)
    {
        $path = $request->pathinfo();

        if ($this->isAuthEndpoint($path)) {
            // 认证接口：1分钟窗口
            return 60;
        } else {
            // 其他接口：1分钟窗口
            return 60;
        }
    }

    /**
     * 检查是否超过限流（使用原子操作避免竞态条件）
     */
    private function checkRateLimit($key, $limit, $window)
    {
        // 尝试使用Redis原子操作
        try {
            $redis = Cache::store('redis')->handler();

            // 使用INCR原子操作
            $current = $redis->incr($key);

            // 第一次访问时设置过期时间
            if ($current === 1) {
                $redis->expire($key, $window);
            }

            // 检查是否超过限制
            if ($current > $limit) {
                return false;
            }

            return true;
        } catch (\Exception $e) {
            // Redis不可用时降级到文件缓存（使用锁机制）
            return $this->checkRateLimitWithLock($key, $limit, $window);
        }
    }

    /**
     * 使用锁机制的限流检查（降级方案）
     */
    private function checkRateLimitWithLock($key, $limit, $window)
    {
        $lockKey = $key . ':lock';
        $maxRetries = 3;

        for ($i = 0; $i < $maxRetries; $i++) {
            // 尝试获取锁
            if (Cache::add($lockKey, 1, 1)) {
                try {
                    $current = Cache::get($key, 0);

                    if ($current >= $limit) {
                        Cache::delete($lockKey);
                        return false;
                    }

                    // 增加计数
                    Cache::set($key, $current + 1, $window);
                    Cache::delete($lockKey);

                    return true;
                } catch (\Exception $e) {
                    Cache::delete($lockKey);
                    throw $e;
                }
            }

            // 等待一小段时间后重试
            usleep(10000); // 10ms
        }

        // 获取锁失败，为了避免阻塞，允许通过
        return true;
    }

    /**
     * 返回限流响应
     */
    private function rateLimitExceeded()
    {
        return json([
            'code' => 429,
            'message' => '请求过于频繁，请稍后再试',
            'data' => null,
            'timestamp' => time()
        ])->code(429);
    }

    /**
     * 是否是认证接口
     */
    private function isAuthEndpoint($path)
    {
        $authPaths = [
            'auth/login',
            'auth/register',
            'auth/wechat-login',
            'auth/alipay-login',
            'auth/douyin-login',
        ];

        foreach ($authPaths as $authPath) {
            if (strpos($path, $authPath) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * 是否是数据接口
     */
    private function isDataEndpoint($path)
    {
        $dataPaths = [
            'accounts',
            'categories',
            'user',
        ];

        foreach ($dataPaths as $dataPath) {
            if (strpos($path, $dataPath) !== false) {
                return true;
            }
        }

        return false;
    }
}